import axios, { AxiosBasicCredentials, AxiosResponse } from "axios";
import { utils } from "elmer-common";

type TypeENV = {
    host: string;
};
export type TypeRuningEnv = {
    DEV: TypeENV;
    SIT: TypeENV;
    UAT: TypeENV;
    PROD: TypeENV;
    PREPROD: TypeENV;
    MOCK: TypeENV;
    LOCAL: TypeENV;
}

export type TypeServiceMethod = "GET" | "POST" | "DELETE" | "PUT" | "OPTIONS";

export interface IServiceEndPointOption {
    withRoute?: boolean;
    withRouteAction?: string;
    withRoutePath?: string;
};

export interface IServiceEndPoint {
    url: string | any;
    method: TypeServiceMethod,
    uri?: any;
    data?: any;
    cookie?: any;
    header?: any;
    withCredentials?: boolean;
    auth?: AxiosBasicCredentials;
    options?: any;
    isDummy?: boolean;
    dummyData?: string;
    timeout?: number;
    responseType?: "json"| "arraybuffer"|"text"|"stream"|"document"|"blob"
};

export interface IServiceNamespace {
    host?: { [H in keyof TypeRuningEnv]?: string };
    isDummy?: boolean;
    endPoints: Record<string, IServiceEndPoint>;
    options?: any;
    withCredentials?: boolean;
};

export interface IServiceConfig<ENV = keyof TypeRuningEnv> {
    env?: ENV;
    isDummy?: boolean;
    host: { [H in keyof TypeRuningEnv]?: string };
    config: Record<string, IServiceNamespace>;
    timeout?: number;
};

export interface IServiceSendOptions {
    endPoint: string;
    uri?: any;
    data?: any;
    header?: any;
    cookie?: any;
    withCredentials?: boolean;
    throwException?: boolean;
    timeout?: number;
    variables?: any;
    isBinary?: boolean;
};
export interface IServiceResponse<IData={}> {
    response: AxiosResponse;
    config: IServiceEndPoint & { id: string };
    endPoint: IServiceEndPoint & { id: string };
    data: IData;
    statusCode: string|number;
    options: IServiceSendOptions;
}
export class ElmerService {
    private config: {[P in keyof IServiceConfig<any>]?: IServiceConfig<any>[P]} = {};
    private env: string;
    constructor() {
        this.env = "DEV";
    }
    setENV(env:string): void {
        this.env = env;
    }
    setConfig(configData: IServiceConfig<any>): void {
        this.config = configData;
    }
    setNamespace(namespace: string, data: IServiceNamespace): void {
        if(this.config?.config) {
            this.config.config[namespace] = data;
        } else {
            this.config.config = {};
            this.config.config[namespace] = data;
        }
    }
    getConfig<T={}>(): {[P in keyof T]: IServiceNamespace} {
        return this.config.config as any;
    }
    getEndPoint(endPointId: string): IServiceEndPoint|undefined|null {
        const endArr = (endPointId || "").split(".");
        const namespace = endArr[0];
        const id = endArr[1];
        const allConfig = this.config?.config;
        const namespaceData = allConfig ? allConfig[namespace] : null;
        if(namespaceData) {
            const endPointConfig = namespaceData.endPoints?.[id];
            if(endPointConfig) {
                const endPoint = JSON.parse(JSON.stringify(endPointConfig));
                const isDummy = this.config.isDummy ?? namespaceData.isDummy ?? endPoint.isDummy;
                const env:any = this.config.env || this.env || "PROD";
                const rootHost = (this.config.host as any)?.[env];
                const namespaceHost = (namespaceData.host as any)?.[env];
                const domainPath = namespaceHost || rootHost;
                let url = endPoint.url;
                if(utils.isObject(endPoint.url)) {
                    url = endPoint.url[env];
                }
                if(isDummy) {
                    url = endPoint.dummyData || url; // 如果未设置dummy数据路径，fallback回之前的设置
                } else {
                    url = domainPath + url;
                }
                if(utils.isObject(endPoint.uri)) { // 将自定义参数合并到连接
                    url = this.mergeLinkAndUri(url, endPoint.uri);
                }
                endPoint.url = url;
                endPoint.options = {
                    ...( namespaceData.options || {} ),
                    ...( endPoint.options || {} )
                };
                endPoint.withCredentials = typeof endPoint.withCredentials === "boolean" ? endPoint.withCredentials : namespaceData.withCredentials;
                endPoint.id = endPointId;
                return endPoint;
            }
        }
    }
    getUrl(endPointId: string): string|undefined|null {
        const endPoint = this.getEndPoint(endPointId);
        return endPoint ? endPoint.url : null;
    }
    /**
     * 发送http请求
     * @param options - 请求参数
     * @param newEndPoint - 自定义API参数配置，不从全局配置数据去读取
     * @returns - 返回Promise对象
     */
    send<T = {}>(options: IServiceSendOptions, newEndPoint?: IServiceEndPoint, extra?: any): Promise<T> {
        return new Promise<T>((resolve, reject) => {
            const endPoint = newEndPoint || this.getEndPoint(options?.endPoint);
            if(!endPoint) {
                reject({
                    message: "The endpoint is not found.",
                    status: "IT-500",
                    endPoint: ((newEndPoint || {}) as any).endPoint || options.endPoint
                });
            } else {
                const sendData = !options.isBinary ? {
                    ...(endPoint.data || {}),
                    ...(options.data || {})
                } : options.data;
                const type = endPoint.method || "GET";
                const headers = {
                    ...(endPoint.header || {}),
                    ...(options.header || {})
                };
                const cookieData = {
                    ...(endPoint.cookie || {}),
                    ...(options.cookie || {})
                };
                const timeout = options.timeout || endPoint?.timeout;
                let url = utils.isObject(options.uri) ? this.mergeLinkAndUri(endPoint.url, options.uri) : endPoint.url;
                if(options.variables) {
                    url = this.replaceVars(url, options.variables);
                }
                this.setCookies(cookieData);
                axios.request({
                    auth: endPoint.auth,
                    data: sendData,
                    headers,
                    method: type,
                    timeout: timeout || this.config.timeout || 30000,
                    url,
                    withCredentials: options.withCredentials || endPoint.withCredentials,
                    responseType: endPoint.responseType || "json",
                }).then((resp: any) => {
                    resolve(({
                        config: endPoint,
                        data: resp.data,
                        endPoint,
                        headers: resp.headers,
                        options: {
                            ...(endPoint.options || {}),
                            ...(options || {})
                        },
                        extra
                    } as any));
                }).catch((error: any) => {
                    // tslint:disable-next-line: no-console
                    console.error(error);
                    const respData = error?.response?.data || error?.data || error;
                    reject({
                        response: error?.response || error,
                        config: endPoint,
                        endPoint,
                        headers: error?.response?.headers || error?.headers,
                        data: error?.response?.data || error?.data || error,
                        statusCode: error?.response?.status || respData.status || respData.statusCode,
                        options: {
                            ...(endPoint.options || {}),
                            ...(options || {})
                        },
                        extra
                    });
                });
            }
        });
    }
    private replaceVars(url: string, vars?: any): string {
        if(!vars) {
            return url;
        } else {
            let checkUrl = url;
            const reg1 = /\{\{[\da-z\._]+\}\}/ig, reg2 = /\{[\da-z\._]+\}/ig;
            const vars1 = url.match(reg1), vars2 = url.match(reg2);
            if(vars1) {
                for(const key of vars1) {
                    while(checkUrl.indexOf(key) >= 0) {
                        const keyValue = key.replace(/^\{\{/, "").replace(/\}\}$/,"");
                        const repValue: any = utils.getValue(vars, keyValue);
                        checkUrl = checkUrl.replace(key, repValue);
                    }
                }
            }
            if(vars2) {
                for(const key of vars2) {
                    while(checkUrl.indexOf(key) >= 0) {
                        const keyValue = key.replace(/^\{/, "").replace(/\}$/,"");
                        const repValue: any = utils.getValue(vars, keyValue);
                        checkUrl = checkUrl.replace(key, repValue);
                    }
                }
            }
            return checkUrl;
        }
    }
    private setCookies(cookieData:any): void {
        if(cookieData) {
            Object.keys(cookieData).forEach((cookieKey):void => {
                const value = cookieData[cookieKey];
                const setValue = utils.isObject(value) ? JSON.stringify(value) : value;
                const cookieValue = `${cookieKey}=${encodeURIComponent(setValue)}`;
                document.cookie = cookieValue;
            });
        }
    }
    /**
     * 将url和uri参数合并
     * @param url 源连接
     * @param uri 合并参数
     * @returns 合并后的链接
     */
    private mergeLinkAndUri(url: string, uri: any): string {
        const uriQuery = this.uriToQuery(uri);
        return !/\?/.test(url) ? url + "?" + uriQuery :
            (/#/.test(url) ? (url as string).replace(/#/, "#" + uriQuery) :
            url + "&" + uriQuery);
    }
    private uriToQuery(uri: any): string {
        const rList: string[] = [];
        if(utils.isObject(uri)) {
            Object.keys(uri).forEach((item: string) => {
                const itemValue = (uri as any)[item];
                const itemV = utils.isObject(itemValue) ? JSON.stringify(itemValue) : itemValue;
                const itemQV = encodeURIComponent(itemV);
                rList.push(`${item}=${itemQV}`);
            });
        }
        return rList.join("&");
    }
}
